﻿//------------------------------------------------------------------------------
// Copyright © FRA & FV 2014
// All rights reserved
//------------------------------------------------------------------------------
// Binary Serialization Custom Tool
//
// SVN revision information:
//   $Revision: 100 $
//------------------------------------------------------------------------------
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using FRAFV.Binary.Serialization.CodeDom;

namespace FRAFV.Binary.Serialization
{
	public sealed class ClassGenerator
	{
		#region Compiler

		internal static Lazy<CodeDomProvider> provider = new Lazy<CodeDomProvider>(() =>
			CodeDomProvider.CreateProvider("CSharp"));
		internal static Lazy<CodeGeneratorOptions> options = new Lazy<CodeGeneratorOptions>(() =>
			new CodeGeneratorOptions
			{
				BracingStyle = "C",
				IndentString = "\t",
				VerbatimOrder = true
			});

		private string bstrInputFileContents;

		public ClassGenerator(string content)
		{
			this.bstrInputFileContents = content;
		}

		#endregion

		internal class MethodContext
		{
			#region Fields
			public readonly ClassParser Class;
			public readonly CodeTypeMember Member;
			protected string itemMethod;
			#endregion

			#region Constructor
			public MethodContext(ClassParser cls, CodeTypeMember member, CodeStatementCollection statements)
			{
				this.Class = cls;
				this.Member = member;
				this.Statements = statements;
			}
			#endregion

			#region Properties
			public CodeStatementCollection Statements { get; protected set; }

			protected virtual string ResolveItemMethod() { return itemMethod; }

			public string ItemMethod
			{
				get
				{
					return ResolveItemMethod();
				}
			}

			#endregion

			#region Resolve method declaration

			public bool GenerateSealedMethod
			{
				get
				{
					return (Class.BinBlock.MethodMode == BinMethodMode.Final || //BinBlock[MethodMode=BinMethodMode.Final]
						Class.IsSealed &&
							(Class.IBinSerializable || //sealed partial class C: IBinSerializable
							 Class.IsBase)); //sealed partial class C
				}
			}

			public bool GenerateBaseMethod
			{
				get
				{
					return (!GenerateSealedMethod &&
						(Class.BinBlock.MethodMode == BinMethodMode.Virtual && !Class.IsSealed || //BinBlock[MethodMode=BinMethodMode.Virtual]
						 Class.IsBase || //partial class C
						 Class.IBinSerializable)); //partial class C: IBinSerializable
				}
			}

			public bool GenerateDerivedMethod
			{
				get
				{
					return (!GenerateSealedMethod &&
						!GenerateBaseMethod);
				}
			}

			protected MemberAttributes MethodsAccess
			{
				get
				{
					var attr = MemberAttributes.Public; //public virtual Method();
					if (GenerateSealedMethod)
						attr |= MemberAttributes.Final; //public Method();
					else if (!GenerateBaseMethod)
						attr |= MemberAttributes.Override; //public override Method();
					return attr;
				}
			}

			#endregion
		}

		internal abstract class MethodGenerator
		{
			protected readonly MethodContext context;
			protected readonly PropertyGenerator[] props;

			protected MethodGenerator(MethodContext context)
			{
				this.context = context;
				this.props = new PropertyGenerator[0];
			}

			protected MethodGenerator(ClassParser cls)
			{
				try
				{
					this.context = CreateMember(cls);
					props = cls.Properties.Select(CreateProperty).Where(prop => prop != null).ToArray();
					AddCompiler(this.context.Member.CustomAttributes);
				}
				catch (InvalidCodeException)
				{
					throw;
				}
				catch (Exception ex)
				{
					throw new InvalidCodeException(ex, cls.Offset);
				}
			}

			protected abstract ClassGenerator.PropertyGenerator CreateProperty(ClassParser.ClassProperty prop);

			#region Invoke expressions

			protected static CodeMethodInvokeExpression MethodInvoke(string method, params object[] args)
			{
				return ((CodeExpression)null).MethodInvoke(method, args);
			}

			protected static CodeMethodInvokeExpression StaticMethodInvoke<TObject>(string method, params object[] args)
			{
				return typeof(TObject).MethodInvoke(method, args);
			}

			protected static CodePropertyReferenceExpression StaticProperty<TObject>(string property)
			{
				return typeof(TObject).Property(property);
			}

			#endregion

			protected CodeStatementCollection Code { get { return context.Statements; } }

			#region Checkers

			public bool HasItemMethod { get { return !context.ItemMethod.IsNullEmpty(); } }

			public bool HasEncoding
			{
				get
				{
					return props.OfType<PropertyDataGenerator>().Any(prop => prop.HasEncoding);
				}
			}

			public bool HasArrayCount
			{
				get
				{
					return props.OfType<PropertyArrayItemGenerator>().Any(prop => prop.HasArrayCount);
				}
			}

			public bool HasCString
			{
				get
				{
					return props.OfType<PropertyDataGenerator>().Any(prop => prop.HasCString);
				}
			}

			public bool GenerateBaseCall
			{
				get
				{
					return (context.Class.BinBlock.MethodMode != BinMethodMode.Abstract && //BinBlock[MethodMode=BinMethodMode.Abstract]
						context.GenerateDerivedMethod);
				}
			}

			#endregion

			internal static void AddCompiler(CodeAttributeDeclarationCollection code)
			{
				var assm = typeof(BinSerializer).Assembly;

				//[System.CodeDom.Compiler.GeneratedCodeAttribute("FRAFV.Binary.Serialization.BinSerializer", "1.0")]
				code.Add<GeneratedCodeAttribute>(typeof(BinSerializer).FullName, assm.GetName().Version.ToString(2));
				//[System.Diagnostics.DebuggerNonUserCodeAttribute()]
				code.Add<System.Diagnostics.DebuggerNonUserCodeAttribute>();
				//[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
				code.Add<System.Runtime.CompilerServices.CompilerGeneratedAttribute>();
			}

			protected CodeExpression GetEncoding()
			{
				if (!context.Class.BinBlock.Encoding.IsNullEmpty())
					//Encoding.GetEncoding("Encoding")
					return StaticMethodInvoke<Encoding>("GetEncoding", context.Class.BinBlock.Encoding);
				else if (context.Class.BinBlock.EncodingCustomMethod.IsNullEmpty())
					//Encoding.UTF8
					return StaticProperty<Encoding>("UTF8");
				else
					//EncodingCustomMethod
					return context.Class.BinBlock.EncodingCustomMethod.AsSnippet();
			}

			protected abstract MethodContext CreateMember(ClassParser cls);
			protected abstract void Init();
			protected virtual void Close() { }

			public CodeTypeMember Generate()
			{
				try
				{
					Init();

					CodeStatementCollection next = null;
					var items = props.OfType<PropertyItemGenerator>();
					PropertyItemGenerator last = items.LastOrDefault();
					foreach (var prop in props)
					{
						var item = prop as PropertyItemGenerator;
						if (item != null)
						{
							if (next == null)
							{
								next = item.Init(this.Code, items) ?? this.Code;
							}

							item.NextStatements = next;
							next = item.Generate() ?? next;

							if (item == last)
							{
								item.Close(this.Code, items);
							}
						}
						else
						{
							prop.Generate();
						}
					}

					Close();

					return context.Member;
				}
				catch (InvalidCodeException)
				{
					throw;
				}
				catch (Exception ex)
				{
					throw new InvalidCodeException(ex, context.Class.Offset);
				}
			}
		}

		internal class PropertyContext : MethodContext
		{
			#region Fields
			public readonly ClassParser.ClassProperty Property;
			#endregion

			#region Constructor
			public PropertyContext(MethodContext context, ClassParser.ClassProperty prop)
				: base(context.Class, context.Member, context.Statements)
			{
				this.Property = prop;
				this.itemMethod = context.ItemMethod;
			}
			#endregion

			#region Properties
			internal CodeStatementCollection NextStatements { set { Statements = value; } }
			public bool IsCustom { get { return Property.BinCustom != null; } }
			public bool IsArray { get { return Property.BinArray != null; } }
			public bool IsConditionArray { get { return !Property.BinArray.ConditionCustomMethod.IsNullEmpty(); } }
			public bool CountIsCustom { get { return !Property.BinArray.CountCustomMethod.IsNullEmpty(); } }
			public bool CountIsNumber { get { return CountNumber > 0; } }
			public bool IsArrayItem { get { return Property.BinArrayItem != null; } }
			public bool IsItem { get { return Property.BinItem.Any(); } }

			public bool CountHasStorage
			{
				get { return !CountIsCustom || Property.BinArray.CountFormat != BinFormat.Auto; }
			}

			public int CountNumber
			{
				get
				{
					int lenNumber;
					return CountIsCustom &&
						int.TryParse(Property.BinArray.CountCustomMethod, NumberStyles.None, null, out lenNumber) ?
						lenNumber : 0;
				}
			}

			protected virtual string ResolveCountMethod(out Type arg) { arg = null; return null; }

			public string CountMethod
			{
				get
				{
					Type lenType;
					return ResolveCountMethod(out lenType);
				}
			}

			public FormatContext SingleFormat
			{
				get
				{
					return Property.BinData.Length == 1 ? new FormatContext(this, Property.BinData[0]) : null;
				}
			}

			public bool IsRepeatItem
			{
				get
				{
					return IsItem && Property.BinItem.FirstOrDefault().ItemUsage == BinItemUsage.Repeatable;
				}
			}

			public bool IsRequiredItem
			{
				get
				{
					return Property.BinItem.Any(attr => attr.ItemUsage == BinItemUsage.Required);
				}
			}
			#endregion
		}

		internal abstract class PropertyGenerator
		{
			protected readonly PropertyContext context;

			protected PropertyGenerator(PropertyContext context)
			{
				this.context = context;
			}

			protected abstract CodeStatementCollection GenerateProperty();

			#region Properties
			public string Name { get { return context.Property.Name; } }
			public string TypeName { get { return context.Property.Type; } }
			public CodeTypeReference Type { get { return context.Property.Type.AsType(); } }
			protected string ArrayTypeName { get { return context.Property.ArrayType; } }
			protected BinCustomAttribute BinCustom { get { return context.Property.BinCustom; } }
			protected BinItemAttribute[] BinItem { get { return context.Property.BinItem; } }
			protected CodeStatementCollection Statements { get { return context.Statements; } }
			internal CodeStatementCollection NextStatements { set { context.NextStatements = value; } }
			#endregion

			#region Resolvers
			protected BinFormat CountFormat { get { return context.Property.BinArray.CountFormat; } }
			protected int CountNumber { get { return context.CountNumber; } }
			protected bool HasCreateCustomMethod { get { return !context.Property.BinArrayItem.CreateCustomMethod.IsNullEmpty(); } }

			protected string ItemsType
			{
				get { return context.Property.Type.IsNullEmpty() ? "" : context.Property.Type + "[]"; }
			}
			#endregion

			#region Expression
			protected CodeSnippetExpression ItemMethod { get { return context.ItemMethod.AsSnippet(); } }
			protected CodeSnippetExpression CountCustomMethod { get { return context.Property.BinArray.CountCustomMethod.AsSnippet(); } }
			protected CodeSnippetExpression ConditionCustomMethod { get { return context.Property.BinArray.ConditionCustomMethod.AsSnippet(); } }
			protected CodeSnippetExpression CreateCustomMethod { get { return context.Property.BinArrayItem.CreateCustomMethod.AsSnippet(); } }
			protected CodeBinaryOperatorExpression PropertyIsNull { get { return context.Property.IsNull(); } }
			protected CodeBinaryOperatorExpression PropertyIsNotNull { get { return context.Property.IsNotNull(); } }

			protected CodeExpression GetArrayCount
			{
				get
				{
					if (context.CountIsCustom)
						return CountCustomMethod;
					if (context.Property.IsCollection)
						return Name.AsField().Property("Count");
					else
						return "buffer".AsVariable().Property("Length");
				}
			}

			#endregion

			public CodeStatementCollection Generate()
			{
				try
				{
					return GenerateProperty();
				}
				catch (InvalidCodeException)
				{
					throw;
				}
				catch (Exception ex)
				{
					throw new InvalidCodeException(ex, context.Property.Offset);
				}
			}
		}

		internal abstract class PropertyArrayItemGenerator : PropertyGenerator
		{
			protected readonly PropertyGenerator arrayGenerator;

			protected PropertyArrayItemGenerator(PropertyContext context, PropertyGenerator array)
				: base(context)
			{
				this.arrayGenerator = array;
			}

			protected override CodeStatementCollection GenerateProperty()
			{
				if (arrayGenerator != null)
					NextStatements = arrayGenerator.Generate();
				return GenerateData();
			}

			protected abstract CodeStatementCollection GenerateData();

			#region Checkers
			public bool HasArrayCount
			{
				get
				{
					return context.IsArray && !context.CountIsNumber && !context.IsConditionArray;
				}
			}
			#endregion
		}

		internal abstract class PropertyItemGenerator : PropertyArrayItemGenerator
		{
			protected PropertyItemGenerator(PropertyContext context, PropertyGenerator array)
				: base(context, array) { }

			public virtual CodeStatementCollection Init(CodeStatementCollection code, IEnumerable<PropertyItemGenerator> props) { return null; }
			public virtual void Close(CodeStatementCollection code, IEnumerable<PropertyItemGenerator> props) { }

			public bool IsRequiredItem
			{
				get { return context.IsRequiredItem; }
			}

			public CodeTypeReference RequiredItemType
			{
				get
				{
					var item = context.Property.BinItem.FirstOrDefault(attr => attr.ItemUsage == BinItemUsage.Required);
					return (item.ResolveItemType() ?? TypeName).AsType();
				}
			}
		}

		internal abstract class PropertyDataGenerator : PropertyArrayItemGenerator
		{
			protected readonly FormatGenerator[] formats;

			protected PropertyDataGenerator(PropertyContext context, PropertyGenerator array)
				: base(context, array)
			{
				try
				{
					this.formats = context.Property.BinData.Select(CreateFormat).Where(binData => binData != null).ToArray();
				}
				catch (InvalidCodeException)
				{
					throw;
				}
				catch (Exception ex)
				{
					throw new InvalidCodeException(ex, context.Property.Offset);
				}
			}

			protected abstract FormatGenerator CreateFormat(BinDataAttribute binData);

			#region Checkers

			public bool HasEncoding
			{
				get
				{
					return formats.Any(format => format.Format.HasEncoding());
				}
			}

			public bool HasCString
			{
				get
				{
					return formats.Any(format => format.Format == BinFormat.CString);
				}
			}

			#endregion

			protected override CodeStatementCollection GenerateData()
			{
				var next = Statements;
				foreach (var format in formats)
				{
					format.NextStatements = next;
					next = format.Generate() ?? next;
				}
				return next;
			}
		}

		internal class FormatContext : PropertyContext
		{
			#region Fields
			public readonly BinDataAttribute BinData;
			public readonly BinFormat Format;
			#endregion

			#region Constructor
			public FormatContext(PropertyContext context, BinDataAttribute binData)
				: base(context, context.Property)
			{
				this.BinData = binData;
				this.Format = FormatManager.ResolveFormat(binData.Format, Property.Type);
			}
			#endregion

			#region Properties
			public bool LengthIsCustom { get { return !BinData.LengthCustomMethod.IsNullEmpty(); } }
			public bool LengthIsNumber { get { return LengthNumber > 0; } }

			public bool LengthHasStorage
			{
				get { return !LengthIsCustom || BinData.LengthFormat != BinFormat.Auto; }
			}

			public int LengthNumber
			{
				get
				{
					int lenNumber;
					return LengthIsCustom &&
						int.TryParse(BinData.LengthCustomMethod, NumberStyles.None, null, out lenNumber) ?
						lenNumber : 0;
				}
			}

			protected virtual string ResolveLengthMethod(out Type arg) { arg = null; return null; }

			public string LengthMethod
			{
				get
				{
					Type lenType;
					return ResolveLengthMethod(out lenType);
				}
			}

			public virtual CodeExpression Encoder
			{
				get { return "encoder".AsVariable(); }
			}

			protected virtual string ResolveProcessMethod(out Type arg) { arg = null; return null; }

			public string ProcessMethod
			{
				get
				{
					Type arg;
					return ResolveProcessMethod(out arg);
				}
			}

			public Type ProcessMethodArgument
			{
				get
				{
					Type arg;
					ResolveProcessMethod(out arg);
					return arg;
				}
			}

			public bool IsPrimitive
			{
				get
				{
					return BinData.Condition.IsNullEmpty() && !Format.HasLength() && !Format.HasEncoding();
				}
			}
			#endregion
		}

		internal abstract class FormatGenerator
		{
			protected readonly FormatContext context;

			protected FormatGenerator(FormatContext context)
			{
				this.context = context;
			}

			#region Resolvers

			public BinFormat Format { get { return context.Format; } }
			protected string PropertyName { get { return context.Property.Name; } }
			protected string PropertyType { get { return context.Property.Type; } }
			protected CodeStatementCollection Statements { get { return context.Statements; } }
			internal CodeStatementCollection NextStatements { set { context.NextStatements = value; } }
			protected BinFormat LengthFormat { get { return context.BinData.LengthFormat; } }
			protected CodeSnippetExpression LengthCustomMethod { get { return context.BinData.LengthCustomMethod.AsSnippet(); } }
			protected int LengthNumber { get { return context.LengthNumber; } }
			protected CodeBinaryOperatorExpression PropertyIsNull { get { return context.Property.IsNull(); } }
			protected CodeBinaryOperatorExpression PropertyIsNotNull { get { return context.Property.IsNotNull(); } }

			internal static CodeExpression ResolveArrayItem(ClassParser.ClassProperty prop)
			{
				if (prop.IsCollection)
					return "item".AsVariable();
				else
					return "buffer".AsVariable().Index("index");
			}

			#endregion

			#region Expressions

			protected CodeExpression GetItem
			{
				get { return context.IsArray ? ResolveArrayItem(context.Property) : PropertyName.AsField(); }
			}

			protected CodeExpression GetItemCast
			{
				get
				{
					var itemCast = GetItem.Cast(PropertyType, Format);
					return Format == BinFormat.CString ? itemCast.Coalesce("") : itemCast;
				}
			}

			protected void AssignItem(CodeExpression value)
			{
				if (context.IsArray && context.Property.IsCollection)
					Statements.MethodInvoke(PropertyName.AsField(), "Add", value.Cast(Format, PropertyType));
				else
					Statements.Assign(GetItem, value.Cast(Format, PropertyType));
			}

			protected CodeExpression GetItemLength
			{
				get
				{
					if (!context.LengthIsCustom)
						return GetItemCast.Property("Length");
					else
						return LengthCustomMethod;
				}
			}

			#endregion

			protected virtual CodeStatementCollection CheckerCondition()
			{
				//if (Condition)
				var cond = context.Statements.Condition(context.BinData.Condition.AsSnippet());
				context.NextStatements = cond.TrueStatements;
				return cond.FalseStatements;
			}

			protected abstract void GenerateData();

			public CodeStatementCollection Generate()
			{
				var next = context.Statements;
				if (!context.BinData.Condition.IsNullEmpty())
					next = CheckerCondition() ?? next;

				GenerateData();

				return next;
			}
		}

		public CodeTypeDeclaration PartialClass(ClassParser cls, Action<string, uint, uint> error)
		{
			var code = new CodeTypeDeclaration(cls.Name) //partial class
			{
				IsClass = true,
				IsPartial = true
			};
			if (!cls.IBinSerializable && (cls.IsBase || cls.BinBlock.MethodMode == BinMethodMode.Final) && cls.Properties.Any())
				code.BaseTypes.Add(typeof(IBinSerializable)); //: IBinSerializable
			foreach (var child in cls.Children)
				code.Members.Add(PartialClass(child, error));
			try
			{
				if (cls.Properties.Any())
				{
					var init = new InitBinaryGenerator(cls);
					code.Members.Add(init.Generate());
					var read = new ReadBinaryGenerator(cls);
					code.Members.Add(read.Generate());
					var size = new DataSizeGenerator(cls);
					code.Members.Add(size.Generate());
					var write = new WriteBinaryGenerator(cls);
					code.Members.Add(write.Generate());
					if (!cls.IEnumerableType.IsNullEmpty())
					{
						var enum1 = new EnumerableGenerator(cls);
						code.Members.Add(enum1.Generate());
					}
				}
			}
			catch (InvalidCodeException ex)
			{
				uint row, column;
				ResolveErrorOffset(bstrInputFileContents, ex, out row, out column);
				error(ex.Message, row, column);
			}
			return code;
		}

		internal static void ResolveErrorOffset(string bstrInputFileContents, InvalidCodeException ex, out uint row, out uint column)
		{
			row = 0; column = 0;
			int pos = ex.Offset;
			foreach (string line in bstrInputFileContents.Split(new string[] { Environment.NewLine }, StringSplitOptions.None))
			{
				if (pos <= line.Length + 1)
				{
					column = (uint)pos;
					break;
				}
				row++;
				pos -= line.Length + 2;
			}
		}

		public string GenerateSerializer(Action<string, uint, uint> error)
		{
			try
			{
				var classes = ClassParser.GetPartials(bstrInputFileContents, error);
				var unit = new CodeCompileUnit();
				var root = new CodeNamespace();
				root.Imports.Add(new CodeNamespaceImport("System"));
				root.Imports.Add(new CodeNamespaceImport("FRAFV.Binary.Serialization"));
				unit.Namespaces.Add(root);
				var ns = classes.FirstOrDefault();
				if (ns != null)
					root.Name = ns.Namespace;
				foreach (var cls in classes)
					root.Types.Add(PartialClass(cls, error));
				using (var sw = new StringWriter())
				{
					provider.Value.GenerateCodeFromCompileUnit(unit, sw, options.Value);
					return sw.ToString();
				}
			}
			catch (InvalidCodeException ex)
			{
				//MessageBox.Show(ex.ToString());
				uint row, column;
				ResolveErrorOffset(bstrInputFileContents, ex, out row, out column);
				error(ex.Message, row, column);
			}
			catch (NotImplementedException ex)
			{
				//MessageBox.Show(ex.ToString());
				error(ex.Message, unchecked((uint)-1), unchecked((uint)-1));
			}
			catch (Exception ex)
			{
				//MessageBox.Show(ex.ToString());
				error(ex.ToString(), unchecked((uint)-1), unchecked((uint)-1));
			}
			return null;
		}
	}
}
